Skip to content

Near-linear basis set dependency handling and level shifting for RHF/UHF/DFT/UDFT#455

Open
vtripath65 wants to merge 39 commits intomerzlab:masterfrom
vtripath65:linear_dep
Open

Near-linear basis set dependency handling and level shifting for RHF/UHF/DFT/UDFT#455
vtripath65 wants to merge 39 commits intomerzlab:masterfrom
vtripath65:linear_dep

Conversation

@vtripath65
Copy link
Collaborator

@vtripath65 vtripath65 commented Mar 9, 2026

Summary

This PR adds robust handling of near-linear basis set dependencies and level shifting to QUICK's SCF and USCF solvers, and fixes several related correctness and stability issues.

Near-linear dependency (canonical orthogonalization)

When the smallest eigenvalue of the overlap matrix falls below the OVCUT threshold (default 1e-5), QUICK now detects the near-linear dependency in fullx and sets NBSuse < nbasis. The transformation matrix x is then rectangular (nbasis × NBSuse) rather than square.

All downstream SCF operations are updated to respect this reduced dimension:

  • quick_overlap_module: fullx now detects near-linear dependency by scanning overlap eigenvalues against OVCUT, logs the number of linearly independent functions, condition number, and smallest eigenvalue, and builds a rectangular x(nbasis, NBSuse) via tmpU(nbasis, NBSuse) and tmpS(NBSuse, NBSuse) scratch arrays.
  • quick_scf_module (RHF/DFT): DIIS error vectors stored as allerror(NBSuse, NBSuse, maxdiis); DIIS transformations use hold3(nbasis, NBSuse) and hold4(NBSuse, NBSuse) rectangular intermediates; diagonalization, level shifting, and MO back-transform all operate in the NBSuse-dimensional subspace. MPI broadcasts use the correct nbasis × NBSuse and NBSuse sizes for co and E.
  • quick_uscf_module (UHF/UDFT): All of the above applied independently to alpha and beta spins; MPI broadcasts corrected for co/cob (nbasis × NBSuse) and E/Eb (NBSuse).

New data fields in quick_qm_struct_type

  • oeff(NBSuse, NBSuse) — reduced-space alpha Fock matrix for diagonalization and level shifting
  • oeffb(NBSuse, NBSuse) — same for beta spin (UHF/UDFT)
  • oldvec(NBSuse, NBSuse) — alpha MO coefficient matrix in the reduced space, used to apply the level shift from the previous iteration
  • oldvecb(NBSuse, NBSuse) — same for beta spin (UHF/UDFT)
  • co(nbasis, NBSuse) and cob(nbasis, NBSuse) — alpha/beta MO coefficient matrices resized from (nbasis, nbasis) to (nbasis, NBSuse)
  • E(NBSuse) and Eb(NBSuse) — orbital energy vectors resized from nbasis to NBSuse

A new allocate_quick_qm_struct_fullx subroutine allocates all dimension-dependent fields (called from fullx after NBSuse is determined).

Level shifting

Level shifting is applied to improve convergence of difficult cases. Three new keywords control the behavior (all can be set in the input file):

Keyword Default Description
LSHIFT_CYCLE 3 Earliest SCF cycle at which level shifting is attempted
LSHIFT_ERR 0.1 Minimum DIIS error required to trigger level shifting
LSHIFT_GAP 0.2 Desired HOMO–LUMO gap enforced by the shift

For UHF/UDFT, alpha and beta shifts are applied independently based on their respective HOMO indices (nelec and nelecb).

Overlap matrix cutoffs

  • OVCUT (default 1e-5): eigenvalue threshold for near-linear dependency detection.
  • ovmatelems (default 1e-6): elements of the overlap matrix smaller than this value are set to zero before diagonalization.

New regression tests

Four new tests covering near-linear dependency cases (all have NBSuse < nbasis):

Input Method Basis
ene_benzene_b3lyp_aug-cc-pvdz B3LYP aug-cc-pVDZ
ene_benzene_ub3lyp_aug-cc-pvdz UB3LYP aug-cc-pVDZ
ene_caffeine_b3lyp_631+gd B3LYP 6-31+G(d)
ene_caffeine_ub3lyp_631+gd UB3LYP 6-31+G(d)

Closes #436.

Remove the eigen vectors of overlap matrix corresponding
low eigen function (less than 1E-05) while forming S^{-1/2}
The debug prints are removed and thresholds are
returned to original values. Arrays are properly
allocated to avoid multiple reallocation and memory
leak.
In the symmetric orthogonalization case (NBSuse == nbasis), oeff is not
allocated, but the SCF DIIS procedure (electdiis) was writing to it in
step 8 and the level-shift block, causing a segfault.

- Step 8 transform: route the X^T.(O.X) result into o instead of oeff
  when NBSuse == nbasis; correct the leading dimension from NBSuse to
  nbasis to match o's declared shape
- Level-shift block: split into canonical/symmetric branches so each
  operates on its own matrix (oeff vs o)
- MAT_DIAG call: conditionally diagonalize oeff or o depending on the
  orthogonalization path
- Ring-buffer, B-matrix, and C=XC' steps likewise split into
  canonical/symmetric branches, reusing hold2/hold instead of hold4/hold5
- Remove unused itererror staging array and hold5 scratch array
- Fix sign of DGEMM 4 (alpha=-1, beta=1) to correctly accumulate
  e(i) = ODS - SDO without a separate subtraction loop
- Add NBSuse declaration to allocate_quick_qm_struct in
  quick_calculated_module.f90
Copilot AI review requested due to automatic review settings March 9, 2026 23:26
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@agoetz agoetz self-requested a review March 10, 2026 22:23
@agoetz agoetz added the enhancement New feature or request label Mar 10, 2026
@agoetz
Copy link
Collaborator

agoetz commented Mar 10, 2026

Currently, CUDA tests stop when running a test that requires f functions and the code was not compiled with f functions. This is also the case with the current master (6369ecd). I am investigating.

Copy link
Collaborator

@agoetz agoetz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great. I think we need to fix minor things and Mulliken charges for two tests seem off, please see comments I made for source code lines.


! basis set number
integer, pointer:: nbasis
integer, pointer:: NBSuse
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason quick_molspec%NBSuse does not really seem to be used.

enddo
enddo
quick_qm_struct%s(Jbas,Ibas) = SJI
! do not consider small overlap matrix elements
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for not considering small overlap matrix elements?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I printed the overlap matrix elements in Gaussian and they were ignoring the small overlap matrix elements. I was experimenting with this to check if we are introducing unnecesdsary noise. I added this during my benzene aug-cc-pvdz tests which I never managed to converge with OVCUT=1.0e-6. We can remove this. Does not affect anything I think.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. If it does not improve convergence then we should not touch the matrix elements. I am trying to think where this could be useful or have an effect - the only thing that comes to mind is sparse matrix operations in linear scaling models. We could leave it in the code but set the default OVCUT to zero.

COEFF = 0.0d0
RHS = 0.0d0
allerror = 0.0d0
if(allocated(allerror)) allerror = 0.0d0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't allerror always allocated, which makes this if check superfluous?

call MAT_DGEMM ('t', 'n', NBSuse, NBSuse, NBSuse, 1.0d0, quick_qm_struct%oldvec, &
NBSuse, quick_scratch%hold2, NBSuse, 0.0d0, quick_qm_struct%o, NBSuse)

homo = quick_molspec%nelec/2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code here is independent of dimension of NBSuse vs nbasis. Should be moved out of the if block

! Standard case (NBSuse == nbasis): diagonalize o(nbasis,nbasis) in place.
RECORD_TIME(timer_begin%TDiag)

if(NBSuse .ne. nbasis) then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where pointers can become useful. If we set

if (NBSuse .ne. nbasis) then
operator => quick_qm_struct%oeff
else
operator => quick_qm_struct%o
end if

then we don't need the if statement here, and probably also not in other places.


! Near-linear dependency: use hold4(NBSuse,NBSuse) as intermediate.
! Standard case: use hold2(nbasis,nbasis) as intermediate.
if(idiis .ge. quick_method%LShift_cycle .and. errormax .gt. quick_method%LShift_err)then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (LShift) then
...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handling of linear dependences

3 participants